package user;

import java.sql.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

import common.AbstractManager;
import common.dal.Transaction;
import common.exceptions.*;
import common.interfaces.IUserRegisteryManager;

/**
 * A <i> Singleton </i> class that Manages all logins, logouts, registerations and unregistrations from the system
 * 
 * @author Gal Lalouche
 */
public class LoginManager extends AbstractManager {
	private final IUserRegisteryManager						userRegisteryManager;
	private static final Map<Long, Session>					sessions		= new ConcurrentHashMap<>();
	// a list of sessions belonging to a user
	private static final Map<String, Collection<Session>>	usersSessions	= new ConcurrentHashMap<>();
	private static final AtomicLong							counter			= new AtomicLong();
	
	private LoginManager(Transaction t) {
		super(t);
		this.userRegisteryManager = new DBPasswordManager(t);
	}
	
	/**
	 * @param t The transaction to use
	 * @return A new instance of a login manager, using the existing transaction
	 */
	static public LoginManager getNewInstance(Transaction t) {
		return new LoginManager(t);
	}
	
	/**
	 * Checks that parameters are not null and throws a wrong password exception if they do not match
	 * 
	 * @param username The username to check
	 * @param password The username's password to check
	 * @throws WrongPasswordException If the password is not the correct password for the user
	 * @throws SQLException
	 */
	public void checkPassword(String username, String password) throws WrongPasswordException, SQLException {
		if (password == null) {
			throw new IllegalArgumentException("password is null");
		}
		if (username == null) {
			throw new IllegalArgumentException("userName is null");
		}
		if (userRegisteryManager.isPasswordOf(username, password) == false) {
			throw new WrongPasswordException(username, password);
		}
	}
	
	//
	// private Transaction setNewTransaction() throws SQLException {
	// Transaction $ = new Transaction();
	// userRegisteryManager.setTransaction($);
	// return $;
	// }
	
	private boolean unregisterUser(String username) throws SQLException {
		try {
			return userRegisteryManager.unregisterUser(username);
		} catch (SQLIntegrityConstraintViolationException e) {
			throw new IllegalStateException("Cannot delete user: " + username
					+ " because there are still messages posted by him", e);
		}
	}
	
	/**
	 * Log user in to the system
	 * 
	 * @param username The user's username
	 * @param password The user's password
	 * @return {@code true} if the user was added to the system; {@code false} if already logged in
	 * @throws WrongPasswordException if the username and the password do not match
	 * @throws SQLException
	 */
	public Session loginUser(String username, String password) throws WrongPasswordException, SQLException {
		checkPassword(username, password);
		Session newSession;
		try {
			newSession = createNewSession(username);
			sessions.put(newSession.getId(), newSession);
			if (usersSessions.containsKey(username) == false) {
				usersSessions.put(username, getNewSessionsCollection());
			}
			Collection<Session> existingSessions = usersSessions.get(username);
			existingSessions.add(newSession);
		} catch (InvalidKeyException e) {
			// already checked in checkPassword
			throw new AssertionError(e);
		}
		return newSession;
	}
	
	private static Collection<Session> getNewSessionsCollection() {
		return Collections.newSetFromMap(new ConcurrentHashMap<Session, Boolean>());
	}
	
	private Session createNewSession(String username) throws SQLException, InvalidKeyException {
		User user = userRegisteryManager.get(username);
		long sessionId = counter.getAndIncrement();
		return new Session(user, sessionId);
	}
	
	/**
	 * Logs the user out the of the system (invalidates the session)
	 * 
	 * @param sessionId The session id to invalidate
	 * @return {@code true} if the user was logged out, {@code false} if he was not logged in to begin with
	 */
	@SuppressWarnings("static-method")
	public boolean logoutUser(long sessionId) {
		Session session = sessions.remove(sessionId);
		if (session != null) {
			User user = session.getUser();
			usersSessions.get(user.getLogin()).remove(session);
		}
		return session != null;
	}
	
	/**
	 * Registers the user to the system
	 * 
	 * @param requestedUsername A unique username to register under
	 * @param name The user's name (can be non-unique)
	 * @param requestedPassword The user's requested password
	 * @return A {@link User} object created by the registration
	 * @throws ExistingEntityException If a user with the request {@code requestedUsername} already exists
	 * @throws SQLException
	 */
	public User registerUser(String requestedUsername, String name, String requestedPassword)
			throws ExistingEntityException, SQLException {
		if (userRegisteryManager.exists(requestedUsername)) {
			throw new ExistingEntityException(requestedUsername);
		}
		try {
			userRegisteryManager.registerUser(requestedUsername, name, requestedPassword);
			return new User(requestedUsername, name);
		} catch (ExistingEntityException e) {
			throw new AssertionError(e);
		}
	}
	
	/**
	 * Registers an administrator to the system<br>
	 * 
	 * @see Administrator
	 * @param requestedUsername A unique username to register under
	 * @param name The user's name (can be non-unique)
	 * @param requestedPassword The user's requested password
	 * @param adminPassword The unique admin password (to assure that only legit administrators are registered)
	 * @return An {@link Administrator} object created by the registration
	 * @throws ExistingEntityException If a user with the request {@code requestedUsername} already exists
	 * @throws WrongAdminPasswordException If the {@code adminPassword} was incorrect
	 * @throws SQLException
	 */
	@Deprecated
	public Administrator registerAdmin(String requestedUsername, String name, String requestedPassword,
			String adminPassword) throws ExistingEntityException, WrongAdminPasswordException, SQLException {
		if (checkAdminPassword(adminPassword) == false) {
			throw new WrongAdminPasswordException(requestedUsername, adminPassword);
		}
		userRegisteryManager.registerAdmin(requestedUsername, name, requestedPassword);
		return new Administrator(requestedUsername, name);
	}
	
	/**
	 * Delete's the user from the system (frees up the username and the user can no longer log in)
	 * 
	 * @param username The user to delete
	 * @param password The user's password
	 * @return {@code true} if the user was deleted successfully, {@code false} if the user wasn't in the system
	 * @throws WrongPasswordException If {@code password} did not match the {@code username}
	 * @throws SQLException
	 */
	public boolean deleteUser(String username, String password) throws WrongPasswordException, SQLException {
		logMethod();
		try {
			if (userRegisteryManager.exists(username) == false) {
				return false;
			}
			checkPassword(username, password);
			boolean result = userRegisteryManager.delete(username);
			
			if (usersSessions.get(username) != null) {
				for (Session session: usersSessions.get(username)) {
					sessions.remove(session.getId());
				}
			}
			usersSessions.remove(username);
			return result;
		} catch (SQLIntegrityConstraintViolationException e) {
			throw new IllegalStateException(e);
		}
	}
	
	// checks that admin exists and is indeed an administrator
	@Deprecated
	private void checkAdmin(Administrator deletingAdmin) throws SQLException, InvalidUserException {
		try {
			if (deletingAdmin.equals(userRegisteryManager.get(deletingAdmin.getLogin())) == false) {
				throw new InvalidKeyException(deletingAdmin.getLogin());
			}
		} catch (InvalidKeyException e) {
			throw new InvalidUserException(deletingAdmin);
		}
	}
	
	/**
	 * Delete's the user from the system (frees up the username and the user can no longer log in) with an administrator
	 * authorization
	 * 
	 * @param username The user to delete
	 * @param deletingAdmin The admin authorizing the delete
	 * @return {@code true} if the user was deleted successfully, {@code false} if the user wasn't in the system
	 * @throws InvalidUserException If the admin is not a registered admin
	 * @throws SQLException
	 */
	@Deprecated
	public boolean deleteUser(String username, Administrator deletingAdmin) throws SQLException,
			InvalidUserException {
		if (userRegisteryManager.exists(username) == false) {
			return false;
		}
		checkAdmin(deletingAdmin);
		return unregisterUser(username);
	}
	
	/**
	 * @param password The password to check
	 * @return {@code true} iff the password is valid admin password
	 * @throws SQLException
	 */
	@Deprecated
	public boolean checkAdminPassword(String password) throws SQLException {
		return userRegisteryManager.checkAdminPassword(password);
	}
	
	/**
	 * Clears all data from the system
	 * 
	 * @throws SQLException
	 */
	@Override
	public void clear() throws SQLException {
		sessions.clear();
		userRegisteryManager.clear();
		counter.set(0);
		logMethod();
	}
	
	/**
	 * Gets the user with the given login
	 * 
	 * @param login The login of the user
	 * @return The user with the given login
	 * @throws InvalidKeyException If no user exists with the login
	 * @throws SQLException
	 */
	public User get(String login) throws InvalidKeyException, SQLException {
		return userRegisteryManager.get(login);
	}
	
	@Override
	public void setup() throws SQLException {
		userRegisteryManager.setup();
		clear();
		logMethod();
	}
	
	@Override
	public boolean drop() throws SQLException {
		logMethod();
		boolean result = userRegisteryManager.drop();
		return result;
	}
	
	/**
	 * @param login The login to check
	 * @return {@code true} iff a user exists with the login
	 * @throws SQLException
	 */
	public boolean exists(String login) throws SQLException {
		return userRegisteryManager.exists(login);
	}
	
	/**
	 * @return all the registered users
	 * @throws SQLException
	 */
	public Collection<User> getAll() throws SQLException {
		return userRegisteryManager.getAll();
	}
	
	/**
	 * @param sessionId The session's id
	 * @return The session
	 * @throws InvalidKeyException If no session exists with the requested id
	 */
	@SuppressWarnings("static-method")
	public Session getSession(long sessionId) throws InvalidKeyException {
		Session session = sessions.get(sessionId);
		if (session != null) {
			return session;
		}
		throw new InvalidKeyException(sessionId);
	}
}
